﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Collections.Specialized;
using Dsl.Ped.Controls.ProjectiveControl.Helpers;

namespace Dsl.Ped.Controls.ProjectiveControl
{
    /// <summary>
    /// This is the <see cref="Control"/> that implements a projection container. Projection container
    /// is a unit grouping the projections and displaying them with respect to the events.
    /// 
    /// The internal workflow of displayed projections is driven by transitions.
    /// </summary>
    [StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(ProjectionControl))]
    public class ProjectionContainerControl : Selector
    {
        /// <summary>
        /// Instance of transitions driving the projection to be displayed
        /// </summary>
        private static readonly DependencyPropertyKey TransitionsPropertyKey = DependencyProperty.RegisterReadOnly(
                "Transitions", typeof(List<TransitionControl>), typeof(ProjectionContainerControl),
                new FrameworkPropertyMetadata(new List<TransitionControl>(), OnTransitionsChanged));

        /// <summary>
        /// Title of the container
        /// </summary>
        public static DependencyProperty TitleProperty;

        /// <summary>
        /// Image of the container
        /// </summary>
        public static DependencyProperty ImageProperty;

        /// <summary>
        /// Transitions of the container
        /// </summary>
        public static DependencyProperty TransitionsProperty = TransitionsPropertyKey.DependencyProperty;

        private Dictionary<string, List<TransitionInfo>> transitionInfoDictionary;

        /// <summary>
        /// Static constructor. Registers all <see cref="DependencyProperty"/> properties
        /// </summary>
        static ProjectionContainerControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(ProjectionContainerControl), new FrameworkPropertyMetadata(typeof(ProjectionContainerControl)));

            TitleProperty = DependencyProperty.Register(
                "Title", typeof(string), typeof(ProjectionContainerControl),
                new FrameworkPropertyMetadata(String.Empty));

            ImageProperty = DependencyProperty.Register(
                "Image", typeof(ImageSource), typeof(ProjectionContainerControl),
                new FrameworkPropertyMetadata(new BitmapImage(new Uri("pack://application:,,,/DSLPedControls;component/Resources/Projection.png", UriKind.RelativeOrAbsolute))));
        }

        /// <summary>
        /// Create new instance of <see cref="ProjectionContainerControl"/>
        /// </summary>
        public ProjectionContainerControl()
        {
            SetValue(TransitionsPropertyKey, new List<TransitionControl>());
        }

        /// <summary>
        /// Create new instance of <see cref="ProjectionContainerControl"/>
        /// </summary>
        /// <param name="transitions">Initial collection of transitions</param>
        public ProjectionContainerControl(List<TransitionControl> transitions)
        {
            SetValue(TransitionsPropertyKey, transitions);
        }

        /// <summary>
        /// Title of this <see cref="ProjectionContainerControl"/>
        /// </summary>
        public string Title
        {
            get { return (string)GetValue(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }

        /// <summary>
        /// <see cref="Image"/> representing this <see cref="ProjectionContainerControl"/>
        /// </summary>
        public ImageSource Image
        {
            get { return (ImageSource)GetValue(ImageProperty); }
            set { SetValue(ImageProperty, value); }
        }

        /// <summary>
        /// List of <see cref="TransitionControl"/> among the <see cref="ProjectionControl"/> within this <see cref="ProjectionContainerControl"/>
        /// </summary>
        public List<TransitionControl> Transitions
        {
            get { return (List<TransitionControl>)GetValue(TransitionsProperty); }
        }

        /// <summary>
        /// Get the list of projections in the container
        /// </summary>
        public List<ProjectionControl> Projections { get; private set; }

        /// <summary>
        /// Method gets executed when the <see cref="ProjectionContainerControl"/> is initialized.
        /// First item of type <see cref="ProjectionControl"/> is set to be selected.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);

            AddTransitionHandlers(this);

            InitializeTransitionsInfo(Transitions);

            if (base.HasItems)
            {
                if (SelectedIndex == -1)
                {
                    base.SetCurrentValue(Selector.SelectedIndexProperty, 0);
                } 
            }
        }

        /// <summary>
        /// Methods gets executed when the items collections is changed.
        /// Each item is checked, whether it is a projection or not, non-projections
        /// are removed from the items.
        /// </summary>
        /// <param name="e">Event changed args</param>
        protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
        {
            base.OnItemsChanged(e);

            if (base.HasItems)
            {
                Projections = new List<ProjectionControl>();

                var nonProjections = new List<object>();
                foreach (var item in Items)
                {
                    if (item is ProjectionControl)
                    {
                        Projections.Add(item as ProjectionControl);
                    } 
                    else
                    {
                        // Item is not a Projection, add it to the list of 
                        // items to be removed
                        nonProjections.Add(item);
                    }
                }

                // Remove items that are not Projection
                RemoveItems(nonProjections);

                // TODO: Test it, added 24.03.2012
                if (Items.Count != 0)
                {
                    SelectedItem = Items[0];
                }
            }
        }

        /// <summary>
        /// Check if the item is its own container
        /// </summary>
        /// <param name="item">Item to be checked</param>
        /// <returns>True if the item is its own container</returns>
        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return (item is ProjectionControl);
        }

        /// <summary>
        /// Remove specific items from <see cref="Items"/>
        /// </summary>
        /// <param name="items">Items to be removed</param>
        #if DEBUG
            public 
        #else
            private
        #endif
        void RemoveItems(IEnumerable<object> items)
        {
            foreach (var item in items)
            {
                Items.Remove(item);
            }
        }

        /// <summary>
        /// Method gets called when the collection of <see cref="TransitionControl"/> is changed.
        /// This method is not invoked when the collection item is added/deleted/changed
        /// </summary>
        /// <param name="obj">Object which has invoked the change</param>
        /// <param name="args">Event arguments</param>
        protected static void OnTransitionsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
        }

        /// <summary>
        /// Initialize collection of <see cref="TransitionInfo"/> with the collection of <see cref="TransitionControl"/>
        /// </summary>
        /// <param name="trans">Initializer of the <see cref="TransitionInfo"/> collection</param>
        private void InitializeTransitionsInfo(IEnumerable<TransitionControl> trans)
        {
            // Just return, the collection is not initialized
            if (trans == null) return;
            
            transitionInfoDictionary = new Dictionary<string, List<TransitionInfo>>();
            foreach (var transition in trans)
            {
                var eventName = EventHelper.GetEventName(transition.Event);
                var eventInfo = EventHelper.GetEventInfoByEventName(this, transition.Event);

                // Event can't be resolved
                if (string.IsNullOrEmpty(eventName)) continue;

                // Get the owner of this event, either empty string (if the owner is this class) or the DeclaringType.Name
                var eventOwner = GetEventOwner(eventInfo);

                // TransitionInfo list should keep the projection instances and Events
                var transitionInfo = new TransitionInfo()
                                    {
                                        FromProjection = transition.FromProjection,
                                        ToProjection = transition.ToProjection,
                                        EventOwner = eventOwner,
                                        EventName = eventName
                                    };

                // Add the transitionInfo to the dictionary of the container
                AddTransitionInfoToDictionary(transitionInfoDictionary, transitionInfo);
            }
        }

        /// <summary>
        /// Add the <see cref="TransitionInfo"/> to the dictionary of this <see cref="ProjectionContainerControl"/>
        /// </summary>
        /// <param name="transitionDictionary">Dictionary of transitions</param>
        /// <param name="transitionInfo"><see cref="TransitionInfo"/> to be put to the dictionary</param>
        #if DEBUG
        public
        #else
        private 
        #endif
        void AddTransitionInfoToDictionary(Dictionary<string, List<TransitionInfo>> transitionDictionary, TransitionInfo transitionInfo)
        {
            List<TransitionInfo> transitions;
            if (transitionDictionary.TryGetValue(transitionInfo.EventName, out transitions))
            {
                transitions.Add(transitionInfo);
            }
            else
            {
                transitionDictionary.Add(transitionInfo.EventName, new List<TransitionInfo>() { transitionInfo });
            }
        }

        /// <summary>
        /// Get owner of this event.
        /// 
        /// Either empty string, if the owner is this class or DeclaringType.FullName of the given <see cref="EventInfo"/>
        /// </summary>
        /// <param name="eventInfo">EventInfo to get owner by</param>
        /// <returns>Owner of the specified <see cref="EventInfo"/> event</returns>
        #if DEBUG
        public
        #else
        private 
        #endif
        string GetEventOwner(EventInfo eventInfo)
        {
            if (eventInfo == null) return string.Empty;

            var eventOwner = (eventInfo.DeclaringType == null) ? string.Empty : eventInfo.DeclaringType.Name;

            if (this.GetType().FullName.Equals(eventInfo.ReflectedType.FullName))
            {
                eventOwner = string.Empty;
            }

            return eventOwner;
        }

        /// <summary>
        /// Add <see cref="EventHandler"/> for any transition event 
        /// </summary>
        /// <param name="container"><see cref="ProjectionContainerControl"/> which should be extended of event handlers</param>
        private void AddTransitionHandlers(ProjectionContainerControl container)
        {
            var transitions = container.Transitions;

            foreach (var tr in transitions)
            {
                if (!string.IsNullOrEmpty(tr.Event))
                {
                    EventInfo info = EventHelper.GetEventInfoByEventName(container, tr.Event);

                    var eventHandlerMethodInfo = typeof(ProjectionContainerControl).GetMethod("OnEventDrivenProjectionEvent", BindingFlags.NonPublic | BindingFlags.Instance);
                    if (eventHandlerMethodInfo == null)
                    {
                        throw new MissingMethodException("ProjectionContainer", "OnEventDrivenProjectionEvent");
                    }

                    var deleg = Delegate.CreateDelegate(info.EventHandlerType, container, eventHandlerMethodInfo);

                    if (tr.Event.Contains("."))
                    {
                        var re = EventHelper.GetEventByEventInfo(info);
                        container.AddHandler(re, deleg);
                    }
                    else
                    {
                        info.AddEventHandler(container, deleg);
                    }
                }
            }
        }

        /// <summary>
        /// Method that gets executed when any registered <see cref="RoutedEvent"/> occures over the 
        /// <see cref="ProjectionContainerControl"/>. The required projection which the current one should transit to
        /// is set as a current <see cref="ProjectionContainerControl.SelectedItem"/>.
        /// </summary>
        /// <param name="sender"><see cref="ProjectionContainerControl"/> where the event has occured</param>
        /// <param name="eventArgs">Argumets of the <see cref="RoutedEvent"/></param>
        private void OnEventDrivenProjectionEvent(object sender, RoutedEventArgs eventArgs)
        {
            var container = (ProjectionContainerControl)sender;
            var eventName = eventArgs.RoutedEvent.Name;
            var eventOwner = eventArgs.RoutedEvent.OwnerType.Name;

            OnEventDrivenProjection(container, eventOwner, eventName);
        }

        /// <summary>
        /// Method that gets executed when any registered <see cref="RoutedEvent"/> occures over the 
        /// <see cref="ProjectionContainerControl"/>. The required projection which the current one should transit to
        /// is set as a current <see cref="ProjectionContainerControl.SelectedItem"/>.
        /// </summary>
        /// <param name="container"><see cref="ProjectionContainerControl"/> where the event has occured</param>
        /// <param name="eventName">Name of the event that has occured in the container</param>
        #if DEBUG
            public 
        #else
            private
        #endif
        void OnEventDrivenProjection(ProjectionContainerControl container, string eventOwner, string eventName)
        {
            if (BehaviourModeHelper.GetCurrentMode() == BehaviorMode.Activation) return;

            if (container == null) return;
            if (string.IsNullOrEmpty(eventName)) return;

            var currentProjection = (ProjectionControl)container.SelectedItem;

            string requiredProjectionName = this.GetProjectionToTransitName(
                container.transitionInfoDictionary, currentProjection.Name, eventOwner, eventName);

            if (string.IsNullOrEmpty(requiredProjectionName))
            {
                return;
            }

            var projection = this.GetProjectionByName(container.Projections, requiredProjectionName);
            if (projection != null) container.SelectedItem = projection;
        }

        /// <summary>
        /// Get the name of projection which the current projection should transit to
        /// </summary>
        /// <param name="transitionsDict">Dictionary of transitions</param>
        /// <param name="fromProjectionName">Name of projection we want to transit from</param>
        /// <param name="eventName">Name of event that initialized the transition</param>
        /// <returns>Name of transition we are going to transit to</returns>
        #if DEBUG
        public 
        #else 
        private
        #endif
        string GetProjectionToTransitName(Dictionary<string, List<TransitionInfo>> transitionsDict, string fromProjectionName, string eventOwner, string eventName)
        {
            if (transitionsDict == null) return string.Empty;
            if (string.IsNullOrEmpty(eventName)) return string.Empty;

            List<TransitionInfo> transitions;
            if (!transitionsDict.TryGetValue(eventName, out transitions))
            {
                return string.Empty;
            }

            if (transitions == null) return string.Empty;

            string defaultToProjection = null;
            foreach (var transition in transitions)
            {
                if (!transition.FromProjection.Equals(fromProjectionName))
                {
                    continue;
                }

                if (string.IsNullOrEmpty(transition.EventOwner))
                {
                    if (defaultToProjection != null)
                    {
                        throw new Exception(string.Format("Event '{0}' has been defined twice for the same projection '{1}'", eventName, fromProjectionName ));
                    }
                    defaultToProjection = transition.ToProjection;
                    continue;
                }

                if (transition.EventOwner.Equals(eventOwner))
                {
                    return transition.ToProjection;
                }
            }

            return defaultToProjection ?? string.Empty;
        }

        /// <summary>
        /// Get the <see cref="ProjectionControl"/> of a given name from the list of projections
        /// </summary>
        /// <param name="projs">List of the projections</param>
        /// <param name="projectionName">Name of the projection from the list</param>
        /// <returns>Projection of a given name from the list of projections</returns>
        #if DEBUG
        public
        #else
        private 
        #endif
        ProjectionControl GetProjectionByName(List<ProjectionControl> projs, string projectionName)
        {
            if (projs == null) return null;

            return (from item in projs where item.Name.Equals(projectionName) select item as ProjectionControl).FirstOrDefault();
        }
    }
}
